# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import os
import sys
_ = os.path.abspath(os.path.dirname(__file__))  # 返回当前文件路径
_ = os.path.abspath(os.path.join(_, '..'))  # 返回根目录文件夹
sys.path.append(_)  # _ 表示上级绝对目录，系统中添加上级目录，可以解决导入不存的问题
sys.path.append('..')  # '..' 表示上级相对目录，系统中添加上级目录，可以解决导入不存的问题
import ccxt
import time
import traceback
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_rows', 1000)
pd.set_option('expand_frame_repr', False)  # 当列太多时不换行
pd.set_option('display.unicode.ambiguous_as_wide', True)  # 设置命令行输出时的列对齐功能
pd.set_option('display.unicode.east_asian_width', True)
from alphalib.product.exchange_config import exchange_basic_config
from alphalib.config_product import data_path, account_config, utc_offset
from alphalib.product.api.market import load_market, fetch_binance_ticker_data
from alphalib.product.api.position import get_spot_position_and_equity, get_position_df, get_equity
from alphalib.product.utils.functions import del_file, refresh_diff_time
from alphalib.product.utils.commons import retry_wrapper
from alphalib.product.utils.dingding import send_wechat_work_msg, send_wechat_work_img
from tqdm import tqdm
from datetime import datetime, timedelta
import dataframe_image as dfi
"""
dataframe_image.export参数简要说明

table_conversion默认是chrome，需要安装chrome，安装麻烦，速度又慢，偶尔卡死进程

table_conversion='matplotlib'，速度快，中文需要修改源码等处理，用英文吧

如果df超过100行会报错，设置 max_rows = -1

如果df超过30列会报错，设置 max_cols = -1

存在计算机生成图片崩溃的可能，主要df别太大
"""
# =画图
plt.rcParams['figure.figsize'] = [12, 4]
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
fmt = '%.2f%%'
yticks = mtick.FormatStrFormatter(fmt)

# 获取数据的公共交易所对象
common_exchange = ccxt.binance(exchange_basic_config)


def orders(exchange, symbol, run_time, hour_offset):
    """
    获取某个币种历史订单数据，做多获取1000条
    :param exchange: 交易所对象
    :param symbol: 币种名
    :param run_time: 运行时间
    :return:
                      time     symbol   price    qty  quoteQty  commission commissionAsset  方向
    0  2023-10-08 19:00:00  AERGOUSDT  0.1024  526.0   53.8624       0.526           AERGO     1
    1  2023-10-08 20:00:00  AERGOUSDT  0.1026  225.0   23.0850       0.225           AERGO     1
    """
    # =设置获取订单时的参数
    params = {
        'symbol': symbol,  # 设置获取订单的币种
        'endTime': int(time.mktime(run_time.timetuple())) * 1000,  # 设置获取订单的截止时间
        'limit': 1000,  # 最大获取1000条订单信息
        'timestamp': ''
    }

    # =调用API获取订单信息
    trades = retry_wrapper(exchange.private_get_mytrades, params=params, func_name='获取币种历史订单信息', if_exit=False)  # 获取账户净值
    # 如果获取订单数据失败，进行容错处理，返回空df
    if trades is None:
        time.sleep(0.5)
        return pd.DataFrame()
    trades = pd.DataFrame(trades, dtype=float)  # 转成df格式

    # =如果获取到的该币种的订单数据是空的，则跳过，继续获取另外一个币种
    if trades.empty:
        time.sleep(0.5)
        return pd.DataFrame()

    # =如果isBuyer为1则为买入，否则为卖出
    trades['方向'] = np.where(trades['isBuyer'] == 1, 1, -1)

    # =整理下有用的数据
    trades = trades[['time', 'symbol', 'price', 'qty', 'quoteQty', 'commission', 'commissionAsset', '方向']]
    # =修改一下time列的格式
    trades['time'] = pd.to_datetime(trades['time'], unit='ms')
    trades['time'] = trades['time'].map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))
    trades['time'] = pd.to_datetime(trades['time'])
    trades['time'] = trades['time'] + timedelta(hours=utc_offset)
    trades['time'] = trades['time'].map(
        lambda x: x.replace(minute=int(hour_offset[:-1]), second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S") if x.minute < (int(hour_offset[:-1]) + 30) else (
                    x.replace(minute=int(hour_offset[:-1]), second=0, microsecond=0) + pd.to_timedelta('1h')).strftime(
            "%Y-%m-%d %H:%M:%S"))
    trades = trades.sort_values('time').reset_index(drop=True)

    # sleep 3秒
    time.sleep(3)

    return trades


def get_orders(exchange, symbol, run_time, start_time=None, end_time=None, default_time='2023-09-28 00:00:00', nums=100, hour_offset='0m'):
    """
    获取某个币种所有的历史订单数据
    :param exchange: 交易所对象
    :param symbol: 币种名
    :param run_time: 运行时间
    :param start_time: 开始时间，非必要
    :param end_time: 结束时间，非必要
    :param default_time: 默认时间，只获取默认时间之后的数据
    :param nums: 循环次数，如果历史订单数据超过1000条，每次取出1000条订单数据，默认循环100次
    :return:
                     time     symbol   price    qty  quoteQty  commission commissionAsset  方向
    0 2023-10-08 19:00:00  AERGOUSDT  0.1024  526.0   53.8624       0.526           AERGO     1
    1 2023-10-08 20:00:00  AERGOUSDT  0.1026  225.0   23.0850       0.225           AERGO     1
    """

    # =定义一个保存订单数据的列表，一次最多能拿到1000条数据（可能拿不完）
    trades_list = []
    # =根据run_time拿到订单数据（最多拿到run_time前1000条订单数据）
    trades = orders(exchange, symbol, run_time=run_time, hour_offset=hour_offset)
    if trades.empty:  # 如果拿到的是空df，即没有历史订单信息，则直接返回空df
        return pd.DataFrame()

    # =因为取一次订单信息最多只能取1000条，可能获取不完，这里会循环获取
    for i in range(nums):
        # =如果取到的订单数据为1000条，则代表实际的订单数据已经超过1000条了，一次api返回的订单数据不全，需要重复获取订单数据
        if len(trades) == 1000:
            # =获取订单时可能只会获取最老时间的一部分，不完整，这里将这个时间截掉
            trades = trades[trades['time'] != trades['time'].min()].reset_index(drop=True)
            if trades.empty:
                break
            # =将删除了第一笔交易时间的数据append到列表中
            trades_list.append(trades)
            # =以此时的第一笔订单交易时间为准，向前推半小时，再次获取这个时间点之前的订单数据
            trades = orders(exchange, symbol, run_time=(pd.to_datetime(trades['time'].iloc[0]) - timedelta(minutes=30)), hour_offset=hour_offset)
        else:
            # =如果得到的trades<1000条，则已经获取了完整的订单数据，将得到的订单数据append到列表中，以后进行合并
            trades_list.append(trades)
            break  # 退出循环
    # =合并数据并整理数据
    all_trades = pd.concat(trades_list, axis=0)
    all_trades = all_trades[all_trades['time'] > default_time]  # 截取默认时间之后的订单数据
    all_trades = all_trades.sort_values('time', ascending=True).reset_index(drop=True)  # 整理数据

    # =如果限定了开始时间和结束时间，则截取下来
    if start_time:
        all_trades = all_trades[all_trades['time'] >= start_time].reset_index(drop=True)
    if end_time:
        all_trades = all_trades[all_trades['time'] <= end_time].reset_index(drop=True)
    # =转化time列的格式
    all_trades['time'] = pd.to_datetime(all_trades['time'])

    return all_trades


def save_position_info(all_trades, path, ticker_price):
    """
    保存币种的历史持仓信息
    :param all_trades: 该币种的所有订单数据
    :param path: 保存路径
    :param ticker_price: 该币种的最新tick价格
    :return:
    """
    # =新建df保存持仓信息
    symbol_info = pd.DataFrame()
    # =取出币种名
    symbol = all_trades['symbol'].iloc[0]
    # =将 成交量 减去 手续费消耗的该币种的数量 得到实际持仓量
    all_trades['qty_real'] = all_trades.apply(lambda x: (x['qty'] - x['commission']) if x['commissionAsset'] == symbol.split('USDT')[0] else x['qty'], axis=1)
    # =成交额按 持仓量/成交量 比例乘一下得到实际持仓额
    all_trades['quoteQty_real'] = all_trades['quoteQty'] * all_trades['qty_real'] / all_trades['qty']

    # =对time进行groupby，将同一时间的订单聚合为一笔交易
    for _time, data in all_trades.groupby('time'):
        # =获取索引
        max_idx = 0 if symbol_info.empty else symbol_info.index.max() + 1
        symbol_info.loc[max_idx, 'time'] = pd.to_datetime(_time)  # 记录时间
        symbol_info.loc[max_idx, 'symbol'] = symbol  # 记录币种
        # =因为存在同一时间既出现买入该币种，又出现卖出该种的情况，所以这里计算成交量和成交额时考虑下方向
        symbol_info.loc[max_idx, '成交量'] = (data['qty_real'] * data['方向']).sum()  # 记录成交量
        symbol_info.loc[max_idx, '成交额'] = (data['quoteQty_real'] * data['方向']).sum()  # 记录成交额
        if symbol_info.loc[max_idx, '成交量'] >= 0:
            symbol_info.loc[max_idx, '方向'] = 1  # 记录方向
        else:
            symbol_info.loc[max_idx, '方向'] = -1  # 记录方向
        # =判断订单数据是否只有一笔
        if len(data) == 1:  # 如果只有一笔数据，则成交均价即为那笔数据的成交价格
            symbol_info.loc[max_idx, '成交均价'] = data['price'].iloc[0]
        else:  # 如果为多笔订单数据，则成交均价为 成交额/成交量
            symbol_info.loc[max_idx, '成交均价'] = symbol_info.loc[max_idx, '成交额'] / symbol_info.loc[max_idx, '成交量']

    # =判断该币种目前有没有本地文件
    if not os.path.exists(path):  # 如果没有本地文件
        # =记录各个时间点的持仓量
        symbol_info['持仓量'] = symbol_info['成交量'].cumsum()
        # =如果以当前价格衡量的持仓额小于5U，则认为该币种已经清仓
        symbol_info['是否清仓'] = np.where((symbol_info['持仓量'] * ticker_price) < 5, True, False)
        # =设置下清仓后的默认持仓量
        symbol_position = 0
        # ===如果该币种之前存在过清仓操作
        if True in symbol_info['是否清仓'].values:
            # =获取最近一次清仓操作的索引
            idx = symbol_info[symbol_info['是否清仓'] == True].index[-1]
            # =更新下清仓之后的持仓量剩余多少(可能卖不干净)
            symbol_position = symbol_info.loc[idx, '持仓量']
            # =截取清仓之后的数据
            symbol_info = symbol_info.iloc[idx + 1:].reset_index(drop=True)
        # =记录各个时间点的持仓额，这里需要考虑下之前卖不干净的历史持仓
        symbol_info['持仓额'] = symbol_info['成交额'].cumsum() + symbol_position * ticker_price
        symbol_info['持仓均价'] = symbol_info['持仓额'] / symbol_info['持仓量']  # 记录各个时间点的持仓均价
        # =删除是否清仓列
        symbol_info.drop('是否清仓', axis=1, inplace=True)

        # =如果币种文件不为空，则将每个币种的最近的一次开仓信息单独保存为一个csv文件
        if not symbol_info.empty:
            symbol_info.to_csv(path, encoding='gbk', index=False)
    else:
        # =如果某个币种有历史持仓数据，则先读取进来
        old_symbol_info = pd.read_csv(path, encoding='gbk')

        # =截取出来新数据(新数据即为历史持仓中没有考虑到的数据，中间有N个小时没有跑，这个时间点直接跑也不会出错)
        symbol_info = symbol_info[symbol_info['time'] > old_symbol_info['time'].iloc[-1]].reset_index(drop=True)
        # =如果拿到的数据都是老数据，已经全部处理过了，则不需要再次进行处理(测试的时候多次运行，不会出现bug)
        if symbol_info.empty:
            return

        # =整理数据
        symbol_info = symbol_info.sort_values('time', ascending=True).reset_index(drop=True)

        # =将新数据与老数据中的最新一条数据合并起来 并 整理
        symbol_info = pd.concat(
            [old_symbol_info[old_symbol_info['time'] == old_symbol_info['time'].max()], symbol_info],
            axis=0)
        symbol_info = symbol_info.reset_index(drop=True)

        # =将 成交量、成交额、方向 修改为 持仓量、持仓额、1，用于之后计算持仓均价
        symbol_info.loc[0, '成交量'] = symbol_info.loc[0, '持仓量']
        symbol_info.loc[0, '成交额'] = symbol_info.loc[0, '持仓额']
        symbol_info.loc[0, '方向'] = 1

        # =计算持仓量、持仓额、持仓均价
        symbol_info['持仓量'] = symbol_info['成交量'].cumsum()
        # =如果根据当前的持仓量、当前的价格计算得到的持仓额小于5U，则将持仓信息中的该币种的文件删除
        symbol_info['是否清仓'] = np.where((symbol_info['持仓量'] * ticker_price) < 5, True, False)
        if True in symbol_info['是否清仓'].values:
            os.remove(path)
            return
        symbol_info['持仓额'] = symbol_info['成交额'].cumsum()
        symbol_info['持仓均价'] = symbol_info['持仓额'] / symbol_info['持仓量']

        # =删除第一条数据(第一条数据为之前合并的老数据)，只保留新数据
        symbol_info = symbol_info.drop(0, axis=0).reset_index(drop=True)
        # =删除是否清仓列
        symbol_info.drop('是否清仓', axis=1, inplace=True)

        # =保存数据
        symbol_info.to_csv(path, encoding='gbk', index=False, header=False, mode='a')


def get_orders_info(new_trades, spot_last_price, symbol):
    """
    根据订单数据获取订单统计信息
    :param new_trades: 最近一小时的订单数据
    :param spot_last_price: 最新各个现货币种的tick价格
    :param symbol: 币种名
    return:
        该币种最近小时的订单统计信息
                     time   symbol 方向  成交均价  平均滑点  成交量    成交额  手续费      滑点亏损  拆分次数      成交价列表     成交量列表         成交额列表  成交最高价与首次交易价相差(%)  成交最低价与首次交易价相差(%)  成交最高价与成交最低价相差(%)
    0 2023-10-30 17:00:00  GNOUSDT    1     104.1       0.0   1.068  111.1788    0.08  1.421085e-14         2  [104.1, 104.1]  [0.96, 0.108]  [99.936, 11.2428]                            0.0                            0.0                            0.0
    """
    # =如果使用BNB来抵扣手续费，则将使用的BNB根据BNB的当前价格转化为U
    if len(new_trades[new_trades['commissionAsset'] == 'BNB']) > 0:
        new_trades.loc[new_trades['commissionAsset'] == 'BNB', 'commission'] *= spot_last_price['BNBUSDT']
        new_trades.loc[new_trades['commissionAsset'] == 'BNB', 'commissionAsset'] = 'USDT'

    # =如果还有其他非USDT的币种来抵扣手续费，则将其也转化为U(比如BTTC币种，在购买BTTC时无法使用BNB进行抵扣，则也将其使用的手续费转化为对应的U)
    if len(new_trades[new_trades['commissionAsset'] != 'USDT']) > 0:
        new_trades.loc[new_trades['commissionAsset'] != 'USDT', 'commission'] *= spot_last_price[symbol]
        new_trades.loc[new_trades['commissionAsset'] != 'USDT', 'commissionAsset'] = 'USDT'

    # ===生成订单信息
    # =新建df统计每个币种的结果
    order_info = new_trades.loc[0, ['time', 'symbol', '方向']].to_frame().T

    if len(new_trades) == 1:  # 如果该币种在该小时内只有一条订单记录
        # =成交均价即为这个交易价格
        order_info['成交均价'] = new_trades['price'].iloc[0]
        # =如果以一笔订单成交则没有滑点
        order_info['平均滑点'] = 0.
    else:  # 如果该币种在该小时内有多条订单记录，则计算成交均价以及平均滑点
        # =成交均价 = 总成交额 / 总成交量
        order_info['成交均价'] = round(new_trades['quoteQty'].sum() / new_trades['qty'].sum(), 6)
        # =平均滑点 = 成交均价 / 第一笔订单的价格 - 1
        order_info['平均滑点'] = round(
            (new_trades['quoteQty'].sum() / new_trades['qty'].sum()) / new_trades['price'].iloc[0] - 1, 6)

    order_info['成交量'] = round(new_trades['qty'].sum(), 6)  # 记录成交量
    order_info['成交额'] = round(new_trades['quoteQty'].sum(), 6)  # 记录成交额
    order_info['手续费'] = round(new_trades['commission'].sum(), 2)  # 记录手续费(以U计价)
    # =计算滑点亏损时需要判断下方向
    if order_info.loc[0, '方向'] == 1:
        # 如果为买入，则 滑点亏损 = 实际发生的成交额 - 以第一笔交易价格计算得到的成交额(没有滑点时的成交额)
        # 比如：以第一笔价格计算得到的成交额为100，而实际发生的成交额为102，则买入该币种多支付了2块钱，即亏损了2块钱，
        # 如果滑点亏损为负，则代表该滑点让你少付了点钱，产生了盈利
        order_info['滑点亏损'] = new_trades['quoteQty'].sum() - new_trades['price'].iloc[0] * new_trades['qty'].sum()
    else:
        # 如果为卖出，则 滑点亏损 = 以第一笔交易价格计算得到的成交额(没有滑点时的成交额) - 实际发生的成交额
        order_info['滑点亏损'] = new_trades['price'].iloc[0] * new_trades['qty'].sum() - new_trades['quoteQty'].sum()
    order_info['拆分次数'] = len(new_trades)  # 记录拆分次数
    order_info['成交价列表'] = [new_trades['price'].to_list()]  # 记录拆分成交的各成交价格
    order_info['成交量列表'] = [new_trades['qty'].to_list()]  # 记录拆分成交的各成交量
    order_info['成交额列表'] = [new_trades['quoteQty'].to_list()]  # 记录拆分成交的各成交额
    # 记录成交最高价与第一笔交易价格的滑点
    order_info['成交最高价与首次交易价相差(%)'] = round(
        order_info['成交价列表'].map(lambda x: 0 if len(x) == 1 else (np.array(x).max() / x[0] - 1)), 6)
    # 记录成交最低价与第一笔交易价格的滑点
    order_info['成交最低价与首次交易价相差(%)'] = round(
        order_info['成交价列表'].map(lambda x: 0 if len(x) == 1 else (np.array(x).min() / x[0] - 1)), 6)
    # 记录成交最高价与成交最低价的滑点
    order_info['成交最高价与成交最低价相差(%)'] = round(
        order_info['成交价列表'].map(lambda x: 0 if len(x) == 1 else (np.array(x).max() / np.array(x).min() - 1)),6)

    return order_info


def get_all_order_info(exchange, symbol_list, run_time, default_time, spot_last_price, account_name, hour_offset):
    """
    监测每小时的交易信息、生成各币种持仓信息
    :param exchange: 交易所duix
    :param symbol_list: 现货币种列表
    :param run_time: 获取订单指定的运行时间
    :param default_time: 获取订单时默认时间，截取默认时间之后的所有历史订单
    :param spot_last_price: 现货各币种的ticker价格
    :param account_name: 账户名称
    return:
                time    symbol 方向    成交均价  平均滑点        成交量      成交额  手续费      滑点亏损  拆分次数                                         成交价列表                                         成交量列表                                         成交额列表  成交最高价与首次交易价相差(%)  成交最低价与首次交易价相差(%)  成交最高价与成交最低价相差(%)
    0   2023-10-13 13:59   ASTUSDT    1    0.082400  0.000000  7.100000e+01    5.850400    0.00 -8.881784e-16         1                                           [0.0824]                                             [71.0]                                           [5.8504]                       0.000000                            0.0                       0.000000
    1   2023-10-13 13:59   XNOUSDT   -1    0.599000  0.000000  1.817900e+02  108.892210    0.08 -1.421085e-14         4                       [0.599, 0.599, 0.599, 0.599]                      [14.84, 17.44, 135.22, 14.29]             [8.88916, 10.44656, 80.99678, 8.55971]                       0.000000                            0.0                       0.000000
    2   2023-10-13 13:59  BTTCUSDT    1    0.000000  0.000000  3.057425e+08  113.124736    0.11  1.421085e-14         2                                 [3.7e-07, 3.7e-07]                          [270270270.0, 35472259.0]                          [99.9999999, 13.12473583]                       0.000000                            0.0                       0.000000
    """

    # =新建持仓信息文件夹
    dir_path = os.path.join(data_path, account_name, '持仓信息')
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

    # =创建一个保存各个币种最近小时订单统计信息的列表
    order_info_list = []

    # ===循环每个币种获取订单统计信息、生成各币种持仓信息
    for symbol in tqdm(symbol_list):
        # =获取到该币种的所有历史订单数据，默认获取 default_time 之后的全部订单数据
        all_trades = get_orders(exchange, symbol, run_time + timedelta(minutes=30), default_time, hour_offset=hour_offset)
        # =判断该币种是否存在历史订单，如果不存在则说明没有交易过该币种，跳过
        if all_trades.empty:
            continue

        # =生成保存文件的路径
        path = os.path.join(dir_path, f'{symbol}.csv')
        # =获取该币种到最新的ticker价格
        ticker_price = spot_last_price[symbol]
        # =保存该币种的持仓信息数据
        save_position_info(all_trades, path, ticker_price)

        # =截取下来最近一小时的订单信息
        new_orders = all_trades[all_trades['time'] > (run_time - timedelta(minutes=30))].reset_index(drop=True)
        # =判断该币种最近一个交易周期是否交易过该币种，如果交易过，则记录下最近的订单数据，如果没有交易过，则跳过
        if new_orders.empty:
            continue
        # =根据该币种的订单数据生成订单统计信息
        order_info = get_orders_info(new_orders, spot_last_price, symbol)
        order_info_list.append(order_info)

    if len(order_info_list) == 0:
        return pd.DataFrame()

    # =将所有币种交易的订单信息合成 总的df
    all_order_info = pd.concat(order_info_list, ignore_index=True)

    return all_order_info


def get_stats_info(buyer, seller, spot_equity):
    """
    获取某个小时所有币种的订单统计数据
    :param buyer: 买入订单数据
    :param seller: 卖出订单数据
    :param spot_equity: 现货账户净值
    :return:
        time  方向  币种数量  成交额  换手率  手续费  逐笔成交最大拆分次数 平均滑点  滑点亏损 最大不利滑点 最大有利滑点
        0  2023-10-13 13:59   1.0       9.0  482.87  15.44%    0.37                   9.0  0.0135%    0.1352      0.1577%         0.0%
        1  2023-10-13 13:59  -1.0       4.0  436.80  13.96%    0.32                   9.0  0.0025%   -0.0109         0.0%      0.1088%
    """

    # =新建df保存结果
    stats_info = pd.DataFrame()

    # =获取订单的统计信息
    # 买入
    if len(buyer) > 0:
        stats_info.loc[0, 'time'] = buyer['time'].mode()[0]  # 记录买入订单时间
        stats_info.loc[0, '方向'] = 1  # 记录买卖方向
        stats_info.loc[0, '币种数量'] = len(buyer)  # 记录买入币种的数量
        stats_info.loc[0, '成交额'] = round(buyer['成交额'].sum(), 2)  # 记录买入成交额
        stats_info.loc[0, '换手率'] = round(buyer['成交额'].sum() / spot_equity, 4)  # 记录买入换手率
        stats_info.loc[0, '手续费'] = round(buyer['手续费'].sum(), 2)  # 记录买入手续费
        stats_info.loc[0, '逐笔成交最大拆分次数'] = buyer['拆分次数'].max()  # 记录买入订单的最大拆分次数
        stats_info.loc[0, '平均滑点'] = round(buyer['平均滑点'].mean(), 6)  # 记录买入订单的平均滑点
        stats_info.loc[0, '滑点亏损'] = round(buyer['滑点亏损'].sum(), 4)  # 记录买入订单的滑点亏损
        stats_info.loc[0, '最大不利滑点'] = round(buyer['成交最高价与首次交易价相差(%)'].max(), 6)  # 记录买入订单的最大不利滑点
        stats_info.loc[0, '最大有利滑点'] = round(buyer['成交最低价与首次交易价相差(%)'].min(), 6)  # 记录买入订单的最大有利滑点
    else:
        stats_info.loc[0, 'time'] = None  # 记录买入订单时间
        stats_info.loc[0, '方向'] = 1  # 记录买卖方向
        stats_info.loc[0, '换手率'] = 0
        stats_info.loc[0, '平均滑点'] = 0
        stats_info.loc[0, '最大不利滑点'] = 0
        stats_info.loc[0, '最大有利滑点'] = 0

    # 卖出
    if len(seller) > 0:
        stats_info.loc[1, 'time'] = seller['time'].mode()[0]  # 记录卖出订单时间
        stats_info.loc[1, '方向'] = -1  # 记录买卖方向
        stats_info.loc[1, '币种数量'] = len(seller)  # 记录卖出币种的数量
        stats_info.loc[1, '成交额'] = round(seller['成交额'].sum(), 2)  # 记录卖出成交额
        stats_info.loc[1, '换手率'] = round(seller['成交额'].sum() / spot_equity, 4)  # 记录卖出换手率
        stats_info.loc[1, '手续费'] = round(seller['手续费'].sum(), 2)  # 记录卖出手续费
        stats_info.loc[1, '逐笔成交最大拆分次数'] = seller['拆分次数'].max()  # 记录卖出订单的最大拆分次数
        stats_info.loc[1, '平均滑点'] = round(seller['平均滑点'].mean(), 6)  # 记录卖出订单的平均滑点
        stats_info.loc[1, '滑点亏损'] = round(seller['滑点亏损'].sum(), 4)  # 记录卖出订单的滑点亏损
        stats_info.loc[1, '最大不利滑点'] = round(seller['成交最低价与首次交易价相差(%)'].min(), 6)  # 记录卖出订单的最大不利滑点
        stats_info.loc[1, '最大有利滑点'] = round(seller['成交最高价与首次交易价相差(%)'].max(), 6)  # 记录卖出订单的最大有利滑点
    else:
        stats_info.loc[1, 'time'] = None  # 记录卖出订单时间
        stats_info.loc[1, '方向'] = -1  # 记录买卖方向
        stats_info.loc[1, '换手率'] = 0
        stats_info.loc[1, '平均滑点'] = 0
        stats_info.loc[1, '最大不利滑点'] = 0
        stats_info.loc[1, '最大有利滑点'] = 0

    return stats_info


def get_order_msg(run_time, order_info, stats_info, buyer, seller):
    """
    发送订单信息
    :param run_time: 运行时间
    :param order_info: 详细订单数据
    :param stats_info: 订单统计数据
    :param buyer: 买入订单数据
    :param seller: 卖出订单数据
    :return:
        订单监测发送信息
            2023-10-18 14:00:00 现货交易
            成交额：851.52
            买入成交额：496.14
            卖出成交额：355.38
            手续费(U)：0.63
            买入手续费(U)：0.37
            卖出手续费(U)：0.26
            总换手率：0.57%
            offset换手率：0.57%
            滑点亏损：0.1735
            买入滑点亏损：0.0592
            卖出滑点亏损：0.1143
            滑点亏损比：0.0204%
    """

    # =判断当前小时是否买入了币种
    if len(buyer) > 0:
        buyer_volume = round(buyer["成交额"].sum(), 2)  # 买入成交额
        buyer_fee = round(buyer["手续费"].sum(), 2)  # 买入手续费
        buyer_slip_loss = round(buyer["滑点亏损"].sum(), 4)  # 买入滑点亏损
    else:  # 如果当前小时没有买入，设置为0
        buyer_volume = 0
        buyer_fee = 0
        buyer_slip_loss = 0

    # =判断当前小时是否卖出了币种
    if len(seller) > 0:
        seller_volume = round(seller["成交额"].sum(), 2)  # 卖出成交额
        seller_fee = round(seller["手续费"].sum(), 2)  # 卖出手续费
        seller_slip_loss = round(seller["滑点亏损"].sum(), 4)  # 卖出滑点亏损
    else:  # 如果当前小时没有卖出，设置为0
        seller_volume = 0
        seller_fee = 0
        seller_slip_loss = 0

    all_volume = round(order_info["成交额"].sum(), 2)  # 总成交额
    all_fee = round(order_info["手续费"].sum(), 2)  # 总手续费
    all_turnover_rate = round(stats_info["换手率"].mean() * 100, 2)  # 总换手率
    all_slip_loss = round(order_info["滑点亏损"].sum(), 4)  # 总滑点亏损
    all_slip_loss_ratio = abs(round(order_info["滑点亏损"].sum() / order_info["成交额"].sum() * 100, 4))  # 滑点亏损占比

    # =企业微信/钉钉中发送的信息
    order_msg = f'{run_time} 现货交易\n'  # 时间 交易类型
    order_msg += f'成交额：{all_volume}\n'  # 总成交额
    order_msg += f'买入成交额：{buyer_volume}\n'  # 买入成交额
    order_msg += f'卖出成交额：{seller_volume}\n'  # 卖出成交额
    order_msg += f'手续费(U)：{all_fee}\n'  # 总手续费
    order_msg += f'买入手续费(U)：{buyer_fee}\n'  # 买入手续费
    order_msg += f'卖出手续费(U)：{seller_fee}\n'  # 卖出手续费
    order_msg += f'总换手率：{all_turnover_rate}%\n'  # 总换手率
    order_msg += f'滑点亏损：{all_slip_loss}\n'  # 总滑点亏损
    order_msg += f'买入滑点亏损：{buyer_slip_loss}\n'  # 买入滑点亏损
    order_msg += f'卖出滑点亏损：{seller_slip_loss}\n'  # 卖出滑点亏损
    order_msg += f'滑点亏损比：{all_slip_loss_ratio}%\n'  # 滑点亏损比

    # =发送买入币种、卖出币种的信息
    bs_msg = ''
    if not buyer.empty:  # 如果存在买入币种信息，则将 买入的币种、买入的U 添加到字符串中
        df_buy = buyer.copy()
        df_buy = df_buy.sort_values('成交额', ascending=False).reset_index(drop=True)
        df_buy.set_index('symbol', inplace=True)
        bs_msg += f'买入现货币种：\n{df_buy[["成交额"]]}\n'
    if not seller.empty:  # 如果存在卖出币种信息，则将 卖出的币种、卖出的U 添加到字符串中
        df_seller = seller.copy()
        df_seller = df_seller.sort_values('成交额', ascending=False).reset_index(drop=True)
        df_seller.set_index('symbol', inplace=True)
        bs_msg += f'卖出现货币种：\n{df_seller[["成交额"]]}\n'

    return order_msg, bs_msg


def save_order_info(order_info, stats_info, run_time, account_name):
    """
    保存订单信息
    :param order_info: 详细订单数据
    :param stats_info: 订单统计数据
    :param run_time: 运行时间
    :param account_name: 账户名称
    :return:
    """

    # ===转换数据格式
    # =转换订单信息的数据格式
    order_info['平均滑点'] = order_info['平均滑点'].map(lambda x: str(round(x * 100, 6)) + '%')
    order_info['成交最高价与首次交易价相差(%)'] = order_info['成交最高价与首次交易价相差(%)'].map(lambda x: str(round(x * 100, 6)) + '%')
    order_info['成交最低价与首次交易价相差(%)'] = order_info['成交最低价与首次交易价相差(%)'].map(lambda x: str(round(x * 100, 6)) + '%')
    order_info['成交最高价与成交最低价相差(%)'] = order_info['成交最高价与成交最低价相差(%)'].map(lambda x: str(round(x * 100, 6)) + '%')

    # =转换统计信息的数据格式
    stats_info['换手率'] = stats_info['换手率'].map(lambda x: str(round(x * 100, 4)) + '%' if str(x) != 'nan' else x)
    stats_info['平均滑点'] = stats_info['平均滑点'].map(lambda x: str(round(x * 100, 6)) + '%' if str(x) != 'nan' else x)
    stats_info['最大不利滑点'] = stats_info['最大不利滑点'].map(lambda x: str(round(x * 100, 6)) + '%' if str(x) != 'nan' else x)
    stats_info['最大有利滑点'] = stats_info['最大有利滑点'].map(lambda x: str(round(x * 100, 6)) + '%' if str(x) != 'nan' else x)

    # =新建订单监测、详细信息文件夹
    dir_path = os.path.join(data_path, account_name, '订单监测', '详细信息')
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

    # =保存订单详细信息
    file_path = os.path.join(dir_path, f'{run_time.strftime("%Y-%m-%d_%H")}_订单详细信息.csv')
    order_info.to_csv(file_path, encoding='gbk', index=False)
    # 删除多余的文件
    del_file(dir_path, 999, file_suffix='.csv')

    # =保存订单统计信息
    dir_path = os.path.join(data_path, account_name, '订单监测', '订单统计信息.csv')
    if os.path.exists(dir_path):  # 如果文件存在，往原有的文件中添加新的结果
        stats_info.to_csv(dir_path, encoding='gbk', index=False, header=False, mode='a')
    else:  # 如果文不件存在，常规的to_csv操作
        stats_info.to_csv(dir_path, encoding='gbk', index=False)


def calc_spot_position(spot_position, account_name):
    """
    发送现货、合约持仓信息
    :param spot_position: 现货持仓
    :param account_name: 账户名称
    :return:
              side  change    pos_u   pnl_u  avg_price  cur_price
    symbol
    ARDRUSDT     1  59.24%   419.98  248.81     0.0515     0.0820
    BTSUSDT      1  36.90%   184.30   68.00     0.0071     0.0097
    GLMUSDT      1  26.67%   691.78  184.49     0.1466     0.1857
    """
    # 初始化两个持仓df
    spot_send_df = pd.DataFrame()
    # =如果现货存在持仓
    if not spot_position.empty:
        # =创建一个保存数据的列表
        position_info_list = []
        # =遍历每个持仓的币种
        for symbol in spot_position.index:
            # =生成路径
            path = os.path.join(data_path, account_name, '持仓信息', f'{symbol}.csv')
            # =判断是否存在持仓数据
            if os.path.exists(path):  # 如果该币种保存过文件，则读取历史持仓数据
                position_info = pd.read_csv(path, encoding='gbk', parse_dates=['time'])
                position_info = position_info[position_info['time'] == position_info['time'].max()]  # 只保留最新的一条数据
                position_info['方向'] = 1  # 方向为1
                # 取出部分列append到列表中
                position_info = position_info[['symbol', '方向', '持仓量', '持仓额', '持仓均价']]
            else:  # 如果没有没存数据，则赋值为nan
                position_info = pd.DataFrame(columns=['symbol', '方向', '持仓量', '持仓额', '持仓均价'], index=[0])
                position_info.loc[0, 'symbol'] = symbol
                position_info.loc[0, '方向'] = 1
                position_info.loc[0, '持仓量'] = spot_position.loc[symbol, '当前持仓量']
                position_info.loc[0, '持仓额'] = np.nan
                position_info.loc[0, '持仓均价'] = np.nan

            # =将读取到的币种持仓数据添加到列表中
            position_info_list.append(position_info)

        # =合并数据
        all_position_info = pd.concat(position_info_list, axis=0)
        # =整理现货持仓数据
        spot_position = spot_position.reset_index()

        # =将现货持仓数据与读取到的数据merge一下
        spot_send_df = pd.merge(all_position_info, spot_position, on='symbol', how='right')
        spot_send_df['change'] = spot_send_df['当前价格'] / spot_send_df['持仓均价'] - 1  # 计算涨跌幅
        spot_send_df.loc[spot_send_df['持仓均价'] < 0, 'change'] = 1 - spot_send_df['当前价格'] / spot_send_df[
            '持仓均价']  # 如果持仓成本为负
        spot_send_df.sort_values('change', ascending=False, inplace=True)  # 以涨跌幅排序
        spot_send_df['pnl_u'] = spot_send_df['change'] * spot_send_df['持仓额']  # 计算现货的持仓盈亏
        spot_send_df.loc[spot_send_df['持仓均价'] < 0, 'pnl_u'] = spot_send_df['change'] * spot_send_df['持仓额'] * -1
        spot_send_df['change'] = spot_send_df['change'].transform(
            lambda x: f'{x * 100:.2f}%' if str(x) != 'nan' else x)  # 最后将数据转成百分比

        # =修改列名
        rename_cols = {'方向': 'side', '持仓额': 'pos_u', '持仓均价': 'avg_price', '当前价格': 'cur_price'}
        spot_send_df.rename(columns=rename_cols, inplace=True)

        # =修改格式并整理
        spot_send_df = spot_send_df[['symbol', 'side', 'change', 'pos_u', 'pnl_u', 'avg_price', 'cur_price']]
        spot_send_df['pos_u'] = spot_send_df['pos_u'].map(lambda x: round(x, 2))
        spot_send_df['pnl_u'] = spot_send_df['pnl_u'].map(lambda x: round(x, 2))
        spot_send_df['avg_price'] = spot_send_df['avg_price'].map(lambda x: round(x, 4))
        spot_send_df['cur_price'] = spot_send_df['cur_price'].map(lambda x: round(x, 4))
        spot_send_df.set_index('symbol', inplace=True)

    return spot_send_df


def calc_swap_position(swap_position):
    """
    发送现货、合约持仓信息
    :param swap_position: 合约持仓
    :return:
              side  change    pos_u   pnl_u  avg_price  cur_price
    symbol
    ARDRUSDT     1  59.24%   419.98  248.81     0.0515     0.0820
    BTSUSDT      1  36.90%   184.30   68.00     0.0071     0.0097
    GLMUSDT      1  26.67%   691.78  184.49     0.1466     0.1857
    """
    swap_send_df = pd.DataFrame()
    # 如果存在合约持仓
    if not swap_position.empty:
        # =整理合约持仓数据
        swap_send_df = swap_position.copy()
        swap_send_df['side'] = swap_send_df['当前持仓量'].apply(
            lambda x: 1 if float(x) > 0 else (-1 if float(x) < 0 else 0))  # 取出方向
        swap_send_df['change'] = (swap_send_df['当前标记价格'] / swap_send_df['均价'] - 1) * swap_send_df['side']  # 计算涨跌幅
        swap_send_df['pos_u'] = swap_send_df['当前持仓量'] * swap_send_df['当前标记价格']  # 计算持仓额
        swap_send_df.rename(columns={'均价': 'avg_price', '持仓盈亏': 'pnl_u', '当前标记价格': 'cur_price'}, inplace=True)  # 修改列名
        swap_send_df = swap_send_df[['side', 'change', 'pos_u', 'pnl_u', 'avg_price', 'cur_price']]
        swap_send_df.sort_values(['side', 'change'], ascending=[True, False], inplace=True)  # 以涨跌幅排序
        swap_send_df['change'] = swap_send_df['change'].transform(lambda x: f'{x * 100:.2f}%' if str(x) != 'nan' else x)  # 最后将数据转成百分比

        # =修改格式
        swap_send_df['pos_u'] = swap_send_df['pos_u'].map(lambda x: round(x, 2))
        swap_send_df['pnl_u'] = swap_send_df['pnl_u'].map(lambda x: round(x, 2))
        swap_send_df['avg_price'] = swap_send_df['avg_price'].map(lambda x: round(x, 4))
        swap_send_df['cur_price'] = swap_send_df['cur_price'].map(lambda x: round(x, 4))

    return swap_send_df


def send_position_result(spot_position, swap_position, account_name):
    """
    发送现货、合约持仓信息
    :param spot_position: 现货持仓
    :param swap_position: 合约持仓
    :param account_name: 账户名称
    :return:
              side  change    pos_u   pnl_u  avg_price  cur_price
    symbol
    ARDRUSDT     1  59.24%   419.98  248.81     0.0515     0.0820
    BTSUSDT      1  36.90%   184.30   68.00     0.0071     0.0097
    GLMUSDT      1  26.67%   691.78  184.49     0.1466     0.1857
    """
    send_spot_df = calc_spot_position(spot_position, account_name)
    send_swap_df = calc_swap_position(swap_position)

    for data in [send_spot_df, send_swap_df]:
        if not data.empty:
            try:
                # =定义导出图片位置
                pos_pic_path = os.path.join(data_path, 'pos.png')
                # =导出图片
                dfi.export(data, pos_pic_path, table_conversion='matplotlib', max_cols=-1, max_rows=-1)
                # =发送图片
                send_wechat_work_img(pos_pic_path, account_config[account_name]['wechat_webhook_url'])
            except BaseException as e:
                print(traceback.format_exc())
                print('持仓数据转换图片出现错误')


def draw_equity_and_send_pic(equity_df, transfer_df, title, webhook_url):
    """
    画资金曲线并发送图片
    :param equity_df:   资金曲线数据
    :param transfer_df: 划转数据
    :param title:       标题
    :param webhook_url: 机器人信息
    """
    # =合并转换划转记录
    equity_df['type'] = 'log'
    equity_df = pd.concat([equity_df, transfer_df], ignore_index=True)
    equity_df.sort_values('time', inplace=True)
    equity_df.reset_index(inplace=True, drop=True)
    # =计算净值
    equity_df = net_fund(equity_df)
    equity_df['net'] = (equity_df['净值'] - 1) * 100
    equity_df['max2here'] = equity_df['净值'].expanding().max()
    equity_df['dd2here'] = (equity_df['净值'] / equity_df['max2here'] - 1) * 100

    # =画图
    fig, ax1 = plt.subplots()
    # 标记买入和卖出点
    buy_signals = equity_df[(equity_df['type'] == 'transfer') & (equity_df['账户总净值'] > 0)]
    sell_signals = equity_df[(equity_df['type'] == 'transfer') & (equity_df['账户总净值'] < 0)]
    ax1.scatter(buy_signals['time'], buy_signals['net'], marker='+', color='black', label='add', s=100)
    ax1.scatter(sell_signals['time'], sell_signals['net'], marker='x', color='red', label='reduce', s=100)
    # 绘图
    ax1.plot(equity_df['time'], equity_df['net'], color='b')
    ax1.grid(True, which='both', linestyle='--', linewidth=0.5)
    plt.title(f'{title} dd2here: {equity_df.iloc[-1]["dd2here"]:.2f}% eq_max: {equity_df["net"].max():.2f}%')
    ax1.yaxis.set_major_formatter(yticks)

    # 创建右侧轴
    # 右轴 回撤
    ax2 = ax1.twinx()
    ax2.fill_between(equity_df['time'], equity_df['dd2here'], 0, color='darkgray', alpha=0.2)
    ax2.set_ylabel('dd2here (%)')
    ax2.yaxis.set_major_formatter(yticks)

    # =定义导出图片位置
    pos_pic_path = os.path.join(data_path, 'pos.png')
    # =保存图片
    plt.savefig(pos_pic_path)
    # =发送图片
    send_wechat_work_img(pos_pic_path, webhook_url)


def save_and_send_equity_info(exchange, swap_equity, swap_position, spot_equity, account_name):
    """
    保存、发送账户信息
    :param exchange:    交易所对象
    :param swap_equity: 合约净值
    :param swap_position: 合约持仓
    :param spot_equity: 现货净值
    :param account_name: 账户名称
    :return:
    """
    # =创建存储账户净值文件目录
    dir_path = os.path.join(data_path, account_name, '账户信息')
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

    # =创建需要存储equity的df
    new_equity_df = pd.DataFrame()
    new_equity_df.loc[0, 'time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')  # 记录时间
    new_equity_df.loc[0, '账户总净值'] = round(spot_equity + swap_equity + swap_position["持仓盈亏"].sum(), 2)  # 记录账户总净值
    new_equity_df.loc[0, '多头现货'] = round(spot_equity, 2)  # 记录多头现货

    # =追加信息到本地存储中
    swap_send_df = calc_swap_position(swap_position)
    if swap_send_df is None or swap_send_df.empty:
        new_equity_df.loc[0, '多头合约'] = 0  # 记录多头合约
        new_equity_df.loc[0, '多头仓位'] = 0  # 记录多头仓位
        new_equity_df.loc[0, '空头仓位'] = 0  # 记录空头仓位
    else:
        new_equity_df.loc[0, '多头合约'] = round(swap_send_df[swap_send_df['side'] == 1]['pos_u'].sum(), 2)  # 记录多头合约
        new_equity_df.loc[0, '多头仓位'] = round(spot_equity + swap_send_df[swap_send_df['side'] == 1]['pos_u'].sum(), 2)  # 记录多头仓位
        new_equity_df.loc[0, '空头仓位'] = round(swap_send_df[swap_send_df['side'] == -1]['pos_u'].sum(), 2)  # 记录空头仓位

    # =新建文件夹路径，保存数据
    equity_file_path = os.path.join(data_path, account_name, '账户信息', 'equity.csv')
    # =判断文件是否存在
    if os.path.exists(equity_file_path):  # 如果存在
        # 读取数据
        equity_df = pd.read_csv(equity_file_path, encoding='gbk', parse_dates=['time'])
        old_equity_df = equity_df[
            equity_df['time'] > datetime.now() - pd.Timedelta(hours=1, minutes=30)]  # 保留近一个半小时的数据，增加一点提前下单造成的时间容错
        # =数据整理
        old_equity_df = old_equity_df.sort_values('time', ascending=True).reset_index(drop=True)

        # =将保存的全部历史账户数据与最新账户数据合并
        equity_df = pd.concat([equity_df, new_equity_df], axis=0)
        equity_df['time'] = pd.to_datetime(equity_df['time'])  # 修改时间格式
        equity_df = equity_df.sort_values('time').reset_index(drop=True)  # 整理数据

        # =记录一下账户总净值的最大值和最小值
        max_all_equity = round(equity_df['账户总净值'].max(), 2)
        min_all_equity = round(equity_df['账户总净值'].min(), 2)

        # ===输出近期走势图
        equity_df = equity_df.reset_index(drop=True)
        _start_time = equity_df.iloc[0]['time']
        # =获取划转记录
        transfer_path = os.path.join(data_path, account_name, '账户信息', 'transfer.csv')
        transfer_df = get_transfer(exchange, transfer_path)
        # =构建近30天数据
        equity_df1 = equity_df.iloc[-720:, :].reset_index(drop=True)
        # 绘图
        draw_equity_and_send_pic(equity_df1, transfer_df, 'equity-curve(last 30 days)', account_config[account_name]['wechat_webhook_url'])

        # =构建近7天数据
        equity_df2 = equity_df.iloc[-168:, :].reset_index(drop=True)
        # 绘图
        draw_equity_and_send_pic(equity_df2, transfer_df, 'equity-curve(last 7 days)', account_config[account_name]['wechat_webhook_url'])
    else:  # 如果不存在，则创建一个新的df
        old_equity_df = pd.DataFrame()
        max_all_equity = np.nan  # 历史最高为nan
        min_all_equity = np.nan  # 历史最低为nan

    # =构建推送消息内容
    equity_msg = f'账户净值： {new_equity_df.loc[0, "账户总净值"]:.2f}\n'
    equity_msg += f'账户： {account_name}\n'
    if old_equity_df.empty:  # 如果是第一次运行，将之前的信息赋值为空
        old_all_equity = np.nan
        old_long_pos = np.nan
        old_short_pos = np.nan
    else:  # 不是第一次运行，则将历史数据用来比较
        old_all_equity = old_equity_df.loc[0, "账户总净值"]
        old_long_pos = old_equity_df.loc[0, "多头仓位"]
        old_short_pos = old_equity_df.loc[0, "空头仓位"]

    equity_msg += f'最近1小时盈亏：{(new_equity_df.loc[0, "账户总净值"] - old_all_equity):.2f}\n'  # 记录近一小时盈亏
    # equity_msg += f'多头最近1小时盈亏：{(new_equity_df.loc[0, "多头仓位"] - old_long_pos):.2f}\n'  # 记录多头最近1小时盈亏
    # equity_msg += f'空头最近1小时盈亏：{(new_equity_df.loc[0, "账户总净值"] - old_all_equity) - (new_equity_df.loc[0, "多头仓位"] - old_long_pos):.2f}\n\n'  # 记录空头最近1小时盈亏

    equity_msg += f'历史最高账户总净值：{max_all_equity}\n'  # 记录历史最高账户净值
    equity_msg += f'历史最低账户总净值：{min_all_equity}\n'  # 记录历史最低账户净值

    # 记录多头仓位、多头现货、多头合约、空头仓位
    equity_msg += f'多头仓位：{new_equity_df.loc[0, "多头仓位"]:.2f}（spot {new_equity_df.loc[0, "多头现货"]:.2f}, swap {new_equity_df.loc[0, "多头合约"]:.2f}）\n'
    equity_msg += f'空头仓位：{new_equity_df.loc[0, "空头仓位"]:.2f}\n'

    # ===计算账户保证金率
    # total_equity = swap_equity + swap_position['持仓盈亏'].sum()  # 计算当前账户总净值（含未实现盈亏）
    # pos_equity = (swap_position['当前标记价格'] * swap_position['当前持仓量']).abs().sum()  # 计算合约账户持仓价值
    # margin_rate = total_equity / pos_equity  # 计算当前账户保证金率
    # equity_msg += f'合约保证金率：{margin_rate:.2f}\n'  # 记录合约保证金率

    # ===计算当前的杠杆倍数
    # lev = (spot_equity + pos_equity) / new_equity_df.loc[0, "账户总净值"]
    # equity_msg += f'当前杠杆倍数：{lev:.2f}\n'  # 记录合约保证金率

    # 保存净值文件
    if os.path.exists(equity_file_path):
        new_equity_df.to_csv(equity_file_path, encoding='gbk', index=False, mode='a', header=False)
    else:
        new_equity_df.to_csv(equity_file_path, encoding='gbk', index=False)

    return equity_msg


def get_history_pos(history_pos_file):
    """
    获取历史持仓快照
    字段:run_time,下单后快照,symbol,type,仓位价值,持仓量,盈亏变化
    """
    if os.path.exists(history_pos_file):
        position_df = pd.read_csv(history_pos_file, parse_dates=['run_time'], encoding='gbk')
        position_df.drop_duplicates(subset=['run_time', '下单后快照', 'symbol', 'type'], keep='last', inplace=True)
        position_df.reset_index(inplace=True, drop=True)
    else:
        position_df = pd.DataFrame()
    return position_df


def save_history_pos(his_pos_df, history_pos_file, max_days=100):
    """
    保存历史持仓快照，只保留最近100天的数据，防止数据量过大，影响速度
    """
    his_pos_df = his_pos_df[
        his_pos_df['run_time'] > his_pos_df['run_time'].iloc[-1] - timedelta(days=max_days)]
    his_pos_df.drop_duplicates(subset=['run_time', '下单后快照', 'symbol', 'type'], keep='last', inplace=True)
    his_pos_df.to_csv(history_pos_file, index=False, encoding='gbk')


def get_swap_position_df(exchange):
    """
    获取U本位合约当前持仓
    """
    position_risk = retry_wrapper(exchange.fapiPrivateV2_get_positionrisk, func_name='获取U本位合约用户持仓')
    position_risk = pd.DataFrame(position_risk, dtype='float')
    columns = {'positionAmt': '当前持仓量', 'notional': '仓位价值'}
    position_risk.rename(columns=columns, inplace=True)
    position_risk = position_risk.loc[
        position_risk['当前持仓量'] != 0, ['symbol', '仓位价值', '当前持仓量']]  # 只保留有仓位的币种
    position_risk.set_index('symbol', inplace=True)  # 将symbol设置为index
    return position_risk


def record_before_order_pos(exchange, run_time, account_name, is_use_spot):
    """
    每小时下单前调用，记录下单前持仓，并计算盈亏变化
    """
    # 获取合约当前持仓
    swap_pos_df = get_swap_position_df(exchange)
    swap_pos_df['run_time'] = run_time
    swap_pos_df['下单后快照'] = 0
    swap_pos_df['type'] = 'swap'
    swap_pos_df = swap_pos_df[['type', '当前持仓量', '仓位价值', 'run_time', '下单后快照']]
    if is_use_spot:
        # 获取现货当前持仓
        _, spot_pos_df, _, _ = get_spot_position_and_equity(exchange)
        spot_pos_df['run_time'] = run_time
        spot_pos_df['下单后快照'] = 0
        spot_pos_df['type'] = 'spot'

    # 读取历史持仓
    dir_path = os.path.join(data_path, account_name, '持仓信息')
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
    history_pos_file = os.path.join(dir_path, '持仓快照.csv')
    his_pos = get_history_pos(history_pos_file)

    if his_pos.shape[0] > 0:
        # 获取上一个小时的持仓快照，如果有下单，则筛选出'下单后快照'为1的快照
        last_pos = his_pos[his_pos['run_time'] == run_time - timedelta(hours=1)]
        last_pos.set_index('symbol', inplace=True)
        temp_df = last_pos[last_pos['下单后快照'] == 1]
        # 如果上一小时有下单后快照，则取下单后的快照
        if len(temp_df) > 0:
            last_pos = temp_df

        # 计算与上一个小时相比，合约和现货持仓的盈亏变化
        swap_pos_df['仓位价值_last'] = last_pos.loc[last_pos['type']=='swap', '仓位价值']
        swap_pos_df['盈亏变化'] = swap_pos_df['仓位价值'] - swap_pos_df['仓位价值_last']
        swap_pos_df.reset_index(drop=False, inplace=True)
        swap_pos_df.sort_values(by=['symbol'], inplace=True)
        swap_pos_df = swap_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量', '盈亏变化']]
        his_pos = his_pos.append(swap_pos_df)
        if is_use_spot:
            spot_pos_df['仓位价值_last'] = last_pos.loc[last_pos['type']=='spot', '仓位价值']
            spot_pos_df['盈亏变化'] = spot_pos_df['仓位价值'] - spot_pos_df['仓位价值_last']
            spot_pos_df.reset_index(drop=False, inplace=True)
            spot_pos_df.sort_values(by=['symbol'], inplace=True)
            spot_pos_df = spot_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量', '盈亏变化']]
            his_pos = his_pos.append(spot_pos_df)
    else:
        # 若历史持仓快照为空，则初始化盈亏变化为0
        swap_pos_df['盈亏变化'] = 0
        swap_pos_df.reset_index(drop=False, inplace=True)
        swap_pos_df.sort_values(by=['symbol'], inplace=True)
        swap_pos_df = swap_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量', '盈亏变化']]
        his_pos = swap_pos_df
        if is_use_spot:
            spot_pos_df['盈亏变化'] = 0
            spot_pos_df.reset_index(drop=False, inplace=True)
            spot_pos_df.sort_values(by=['symbol'], inplace=True)
            spot_pos_df = spot_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量', '盈亏变化']]
            his_pos = his_pos.append(spot_pos_df)

    # print('【下单前合约持仓：】\n', swap_pos_df)
    # if is_use_spot:
    #     print('【下单前现货持仓：】\n', spot_pos_df)
    if not his_pos.empty:
        save_history_pos(his_pos, history_pos_file)


def record_after_order_pos(exchange, run_time, account_name, is_use_spot):
    """
    下单后调用，记录下单后持仓
    """
    dir_path = os.path.join(data_path, account_name, '持仓信息')
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
    history_pos_file = os.path.join(dir_path, '持仓快照.csv')
    his_pos = get_history_pos(history_pos_file)

    # 获取合约当前持仓
    swap_pos_df = get_swap_position_df(exchange)
    swap_pos_df['run_time'] = run_time
    swap_pos_df['下单后快照'] = 1
    swap_pos_df['type'] = 'swap'
    swap_pos_df.reset_index(drop=False, inplace=True)
    swap_pos_df.sort_values(by=['symbol'], inplace=True)
    swap_pos_df = swap_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量']]
    his_pos = his_pos.append(swap_pos_df)
    # print('【下单后合约持仓：】\n', swap_pos_df)
    if is_use_spot:
        # 获取现货当前持仓
        _, spot_pos_df, _, _ = get_spot_position_and_equity(exchange)
        spot_pos_df['run_time'] = run_time
        spot_pos_df['下单后快照'] = 1
        spot_pos_df['type'] = 'spot'
        spot_pos_df.reset_index(drop=False, inplace=True)
        spot_pos_df.sort_values(by=['symbol'], inplace=True)
        spot_pos_df = spot_pos_df[['run_time', '下单后快照', 'symbol', 'type', '仓位价值', '当前持仓量']]
        his_pos = his_pos.append(spot_pos_df)
        # print('【下单后现货持仓：】\n', spot_pos_df)

    if '盈亏变化' not in his_pos.columns:
        his_pos['盈亏变化'] = np.nan

    if not his_pos.empty:
        save_history_pos(his_pos, history_pos_file)


def get_pnl_change(account_name, run_time, m, n=5):
    """
    获取run_time之前m小时的盈亏变化top n
    """
    dir_path = os.path.join(data_path, account_name, '持仓信息')
    history_pos_file = os.path.join(dir_path, '持仓快照.csv')
    his_pos = get_history_pos(history_pos_file)
    if his_pos.empty:
        return ''

    his_pos = his_pos[(his_pos['run_time'] > run_time - timedelta(hours=m)) & (his_pos['下单后快照'] == 0)]
    his_pos['方向'] = np.where(his_pos['当前持仓量'] > 0, '1', '-1')
    his_pos['symbol'] = his_pos['symbol'] + '(' + his_pos['type'] + ' ' + his_pos['方向'] + ')'

    # 统计所有symbol的盈亏变化
    pnl_change = his_pos.groupby('symbol')['盈亏变化'].sum().to_frame()
    pnl_change.reset_index(drop=False, inplace=True)

    profit = pnl_change.sort_values(by='盈亏变化', ascending=False)
    profit['盈亏变化'] = round(profit['盈亏变化'], 2).apply(str)
    profit = profit.head(n)
    loss = pnl_change.sort_values(by='盈亏变化')
    loss['盈亏变化'] = round(loss['盈亏变化'], 2).apply(str)
    loss = loss.head(n)

    res = f'{m}小时内盈利top{n}：\n'
    res += profit.to_json(orient='values').replace('"','').replace('],[', '\n').replace('[[','').replace(']]','').replace(',', ': ')
    res += f'\n{m}小时内亏损top{n}：\n'
    res += loss.to_json(orient='values').replace('"','').replace('],[', '\n').replace('[[','').replace(']]','').replace(',', ': ')

    return res


def get_transfer(exchange, transfer_path):
    """
    获取划转记录

    MAIN_UMFUTURE 现货钱包转向U本位合约钱包
    MAIN_MARGIN 现货钱包转向杠杆全仓钱包

    UMFUTURE_MAIN U本位合约钱包转向现货钱包
    UMFUTURE_MARGIN U本位合约钱包转向杠杆全仓钱包

    CMFUTURE_MAIN 币本位合约钱包转向现货钱包

    MARGIN_MAIN 杠杆全仓钱包转向现货钱包
    MARGIN_UMFUTURE 杠杆全仓钱包转向U本位合约钱包

    MAIN_FUNDING 现货钱包转向资金钱包
    FUNDING_MAIN 资金钱包转向现货钱包

    FUNDING_UMFUTURE 资金钱包转向U本位合约钱包
    UMFUTURE_FUNDING U本位合约钱包转向资金钱包

    MAIN_OPTION 现货钱包转向期权钱包
    OPTION_MAIN 期权钱包转向现货钱包

    UMFUTURE_OPTION U本位合约钱包转向期权钱包
    OPTION_UMFUTURE 期权钱包转向U本位合约钱包

    MAIN_PORTFOLIO_MARGIN 现货钱包转向统一账户钱包
    PORTFOLIO_MARGIN_MAIN 统一账户钱包转向现货钱包

    MAIN_ISOLATED_MARGIN 现货钱包转向逐仓账户钱包
    ISOLATED_MARGIN_MAIN 逐仓钱包转向现货账户钱包

    :param exchange:    交易所对象
    :param run_time:    运行时间
    """

    start_time = datetime.now().replace(minute=0, second=0, microsecond=0) - pd.Timedelta(days=180)
    add_type = ['CMFUTURE_MAIN', 'MARGIN_MAIN', 'MARGIN_UMFUTURE', 'FUNDING_MAIN', 'FUNDING_UMFUTURE',
                'OPTION_MAIN', 'OPTION_UMFUTURE', 'PORTFOLIO_MARGIN_MAIN', 'ISOLATED_MARGIN_MAIN']
    reduce_type = ['MAIN_MARGIN', 'UMFUTURE_MARGIN', 'MAIN_FUNDING', 'UMFUTURE_FUNDING', 'MAIN_OPTION',
                   'UMFUTURE_OPTION', 'MAIN_PORTFOLIO_MARGIN', 'MAIN_ISOLATED_MARGIN']

    result = []
    for _ in add_type + reduce_type:
        params = {
            'fromSymbol': 'USDT',
            'startTime': int(start_time.timestamp() * 1000),
            'type': _,
            'timestamp': int(round(time.time() * 1000)),
            'size': 100,
        }
        if _ == 'MAIN_ISOLATED_MARGIN':
            params['toSymbol'] = 'USDT'
            del params['fromSymbol']
        # 获取划转信息(取上一小时到当前时间的划转记录)
        try:
            # account_info = retry_wrapper(exchange.sapi_get_asset_transfer, params=params, func_name='获取万向划转历史', if_exit=False)
            account_info = exchange.sapi_get_asset_transfer(params)
        except:
            print(f'当前账户查询类型【{_}】失败，不影响后续操作，请忽略')
            continue
        if account_info and int(account_info['total']) > 0:
            res = pd.DataFrame(account_info['rows'])
            res['timestamp'] = pd.to_datetime(res['timestamp'], unit='ms')
            res.loc[res['type'].isin(add_type), 'flag'] = 1
            res.loc[res['type'].isin(reduce_type), 'flag'] = -1
            res = res[res['status'] == 'CONFIRMED']
            result.append(res)

    # 获取主账号与子账号之间划转记录
    result2 = []
    for transfer_type in [1, 2]:  # 1: 划入。从主账号划转进来  2: 划出。从子账号划转出去
        params = {
            'asset': 'USDT',
            'type': transfer_type,
            'startTime': int(start_time.timestamp() * 1000),
        }
        try:
            # account_info = retry_wrapper(exchange.sapi_get_sub_account_transfer_subuserhistory, params=params, func_name='获取账户间划转历史', if_exit=False)
            account_info = exchange.sapi_get_sub_account_transfer_subuserhistory(params)
        except:
            print(f'当前账户查询类型【{transfer_type}】失败，不影响后续操作，请忽略')
            continue
        if account_info and len(account_info):
            res = pd.DataFrame(account_info)
            res['time'] = pd.to_datetime(res['time'], unit='ms')
            res.rename(columns={'qty': 'amount', 'time': 'timestamp'}, inplace=True)
            res.loc[res['toAccountType'] == 'SPOT', 'flag'] = 1 if transfer_type == 1 else -1
            res.loc[res['toAccountType'] == 'USDT_FUTURE', 'flag'] = 1 if transfer_type == 1 else -1
            res = res[res['status'] == 'SUCCESS']
            res = res[res['toAccountType'].isin(['SPOT', 'USDT_FUTURE'])]
            result2.append(res)

    # 将账号之间的划转与单账号内部换转数据合并
    result.extend(result2)
    if not len(result):
        return pd.DataFrame()

    all_df = pd.concat(result, ignore_index=True)
    all_df.drop_duplicates(subset=['timestamp', 'tranId', 'flag'], inplace=True)
    all_df.sort_values('timestamp', inplace=True)

    all_df['amount'] = all_df['amount'].astype(float) * all_df['flag']
    all_df.rename(columns={'amount': '账户总净值'}, inplace=True)
    all_df['type'] = 'transfer'
    all_df = all_df[['timestamp', '账户总净值', 'type']]
    all_df['timestamp'] = all_df['timestamp'] + pd.Timedelta(hours=utc_offset)
    all_df.reset_index(inplace=True, drop=True)

    all_df['time'] = all_df['timestamp']
    result_df = all_df.resample(rule='1H', on='timestamp').agg({'time': 'last', '账户总净值': 'sum', 'type': 'last'})
    result_df = result_df[result_df['type'].notna()]
    result_df.reset_index(inplace=True, drop=True)

    if os.path.exists(transfer_path):
        exist_transfer_df = pd.read_csv(transfer_path, encoding='gbk', parse_dates=['time'])
        result_df = pd.concat([exist_transfer_df, result_df], axis=0)
        result_df = result_df.drop_duplicates(keep='first').reset_index(drop=True)
        result_df.to_csv(transfer_path, encoding='gbk', index=False)
    else:
        result_df.to_csv(transfer_path, encoding='gbk', index=False)

    return result_df


def net_fund(df):
    first_log_index = (df['type'] == 'log').idxmax()
    df = df.loc[first_log_index:, :]
    df.reset_index(inplace=True, drop=True)

    df.loc[0, '净值'] = 1
    df.loc[0, '份额'] = df.iloc[0]['账户总净值'] / df.iloc[0]['净值']
    df.loc[0, '当前总市值'] = df.iloc[0]['账户总净值']
    for i in range(1, len(df)):
        if df.iloc[i]['type'] == 'log':
            df.loc[i, '当前总市值'] = df.iloc[i]['账户总净值']
            df.loc[i, '份额'] = df.iloc[i - 1]['份额']
            df.loc[i, '净值'] = df.iloc[i]['当前总市值'] / df.loc[i]['份额']
        if df.iloc[i]['type'] == 'transfer':
            reduce_cnt = df.iloc[i]['账户总净值'] / df.iloc[i - 1]['净值']
            df.loc[i, '份额'] = df.loc[i - 1]['份额'] + reduce_cnt
            df.loc[i, '当前总市值'] = df.iloc[i]['账户总净值'] + df.iloc[i - 1]['当前总市值']
            df.loc[i, '净值'] = df.iloc[i]['当前总市值'] / df.iloc[i]['份额']

    return df


def run():
    import sys
    if len(sys.argv) > 1:
        timestamp = sys.argv[1]
        run_time = datetime.fromtimestamp(int(timestamp))
    else:
        run_time = None
    print(run_time)
    # =====刷新一下与交易所的时差
    refresh_diff_time(common_exchange)
    # =设置一下默认时间，用于获取订单时截取订单数据
    default_time = '2023-09-28 00:00:00'

    # =====获取合约与现货的信息。最小下单量，下单精度等
    # =获取U本位合约交易对的信息
    # swap_symbol_list, swap_min_qty, swap_price_precision, swap_min_notional = load_market(common_exchange, symbol_type='swap')
    # =加载现货交易对信息
    spot_symbol_list, spot_min_qty, spot_price_precision, spot_min_notional = load_market(common_exchange, symbol_type='spot')

    # =====遍历所有账号监测
    for account_name, account_info in account_config.items():
        # ===获取账号的配置
        exchange = account_info['exchange']
        has_spot_strategy = account_info['has_spot_strategy']
        wechat_webhook_url = account_info['wechat_webhook_url']
        hour_offset = account_info['hour_offset']
        try:
            swap_position = get_position_df(exchange)
            swap_equity, _, _ = get_equity(exchange)
            if has_spot_strategy:
                spot_equity, spot_position, spot_usdt, dust_spot = get_spot_position_and_equity(exchange)
                spot_position = spot_position
                spot_equity = spot_equity
            else:
                spot_position = pd.DataFrame()
                spot_equity = 0
        except:
            traceback.print_exc()
            print(f'当前账号【{account_name}】，获取数据失败')
            continue

        # =====获取当前运行小时
        if run_time is None:
            run_time = datetime.now().replace(minute=int(hour_offset[:-1]), second=0, microsecond=0)
            # 仅仅在run time是none时，进行判断
            if not (datetime.now() - pd.to_timedelta('5m') <= run_time <= datetime.now() + pd.to_timedelta('5m')):
                print(f'当前账号【{account_name}】未到时间统计')
                continue
        # 以下代码可以测试的时候使用
        # run_time = datetime.strptime('2023-10-31 11:00:00', "%Y-%m-%d %H:%M:%S")
        if run_time.minute != int(hour_offset[:-1]):
            print(f'当前账号【{account_name}】未到时间统计')
            continue

        # ===生成账户净值信息
        equity_msg = save_and_send_equity_info(exchange, swap_equity, swap_position, spot_equity, account_name)
        # =发送账户净值信息
        send_wechat_work_msg(equity_msg, wechat_webhook_url)

        # =获取一下现货各个币种的最新价格
        spot_last_price = fetch_binance_ticker_data(exchange, symbol_type='spot')

        # =====每小时交易的订单监测、生成历史持仓信息(只保留当前持仓的文件，历史交易过的且已经清仓的不会保留文件)
        if has_spot_strategy:
            # ===获取订单的详细信息、以及生成各币种的持仓信息

            # ==new读取账户的换仓信息
            try:
                filename = run_time.strftime("%Y%m%d_%H") + ".csv"
                select_symbol_list_path = os.path.join(data_path, account_name, '账户换仓信息', filename)
                select_symbol = pd.read_csv(select_symbol_list_path, encoding='gbk')
                select_symbol_list = select_symbol.loc[select_symbol['symbol_type']=='spot','symbol'].tolist()
                all_spot_order_info = get_all_order_info(exchange, sorted(select_symbol_list), run_time, default_time, spot_last_price, account_name, hour_offset)
            except Exception as err:
                print('读取账户换仓信息出错，出错原因: ' + str(err))
                all_spot_order_info = get_all_order_info(exchange, sorted(spot_symbol_list), run_time, default_time, spot_last_price, account_name, hour_offset)

            # 判断当前小时是否存在订单信息
            if all_spot_order_info.empty:
                print('该时间不存在订单，不发送订单监测信息...')
            else:
                # =整理订单数据
                all_spot_order_info = all_spot_order_info.sort_values(['方向', 'time', 'symbol']).reset_index(drop=True)
                # =将订单数据拆分为买入和卖出
                buyer = all_spot_order_info[all_spot_order_info['方向'] == 1].reset_index(drop=True)
                seller = all_spot_order_info[all_spot_order_info['方向'] == -1].reset_index(drop=True)

                # =生成该小时的统计数据，一共两行，一行为买入，一行为卖出
                stats_info = get_stats_info(buyer, seller, spot_equity)

                # =获取发送的订单监测信息
                order_msg, bs_msg = get_order_msg(run_time, all_spot_order_info, stats_info, buyer, seller)

                # =保存订单监测信息
                save_order_info(all_spot_order_info, stats_info, run_time, account_name)

                # =发送订单监测信息
                if order_msg:
                    send_wechat_work_msg(order_msg, wechat_webhook_url)
                # =发送买卖币种信息
                if bs_msg:
                    send_wechat_work_msg(bs_msg, wechat_webhook_url)

                # =清理数据
                del buyer, seller, stats_info, order_msg, bs_msg

        # ===发送各币种持仓信息
        send_position_result(spot_position, swap_position, account_name)

        # 统计过去m小时的盈亏top n
        pnl_chg = get_pnl_change(account_name, run_time, 1)
        if pnl_chg:
            pnl_chg += '\n\n' + get_pnl_change(account_name, run_time, 12) + '\n'
            send_wechat_work_msg(pnl_chg, wechat_webhook_url)

        # 休息一会儿，避免超权重
        time.sleep(60)


if __name__ == '__main__':
    try:
        run()
    except Exception as err:
        msg = '保3下单统计脚本出错，出错原因: ' + str(err)
        print(msg)
        print(traceback.format_exc())
